/*
 * Copyright (c) 2003, 2008, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing;

import javax.swing.table.*;
import java.awt.*;
import java.awt.print.*;
import java.awt.geom.*;
import java.text.MessageFormat;

/**
 * An implementation of <code>Printable</code> for printing
 * <code>JTable</code>s.
 * <p>
 * This implementation spreads table rows naturally in sequence
 * across multiple pages, fitting as many rows as possible per page.
 * The distribution of columns, on the other hand, is controlled by a
 * printing mode parameter passed to the constructor. When
 * <code>JTable.PrintMode.NORMAL</code> is used, the implementation
 * handles columns in a similar manner to how it handles rows, spreading them
 * across multiple pages (in an order consistent with the table's
 * <code>ComponentOrientation</code>).
 * When <code>JTable.PrintMode.FIT_WIDTH</code> is given, the implementation
 * scales the output smaller if necessary, to ensure that all columns fit on
 * the page. (Note that width and height are scaled equally, ensuring that the
 * aspect ratio remains the same).
 * <p>
 * The portion of table printed on each page is headed by the
 * appropriate section of the table's <code>JTableHeader</code>.
 * <p>
 * Header and footer text can be added to the output by providing
 * <code>MessageFormat</code> instances to the constructor. The
 * printing code requests Strings from the formats by calling
 * their <code>format</code> method with a single parameter:
 * an <code>Object</code> array containing a single element of type
 * <code>Integer</code>, representing the current page number.
 * <p>
 * There are certain circumstances where this <code>Printable</code>
 * cannot fit items appropriately, resulting in clipped output.
 * These are:
 * <ul>
 * <li>In any mode, when the header or footer text is too wide to
 * fit completely in the printable area. The implementation
 * prints as much of the text as possible starting from the beginning,
 * as determined by the table's <code>ComponentOrientation</code>.
 * <li>In any mode, when a row is too tall to fit in the
 * printable area. The upper most portion of the row
 * is printed and no lower border is shown.
 * <li>In <code>JTable.PrintMode.NORMAL</code> when a column
 * is too wide to fit in the printable area. The center of the
 * column is printed and no left and right borders are shown.
 * </ul>
 * <p>
 * It is entirely valid for a developer to wrap this <code>Printable</code>
 * inside another in order to create complex reports and documents. They may
 * even request that different pages be rendered into different sized
 * printable areas. The implementation was designed to handle this by
 * performing most of its calculations on the fly. However, providing different
 * sizes works best when <code>JTable.PrintMode.FIT_WIDTH</code> is used, or
 * when only the printable width is changed between pages. This is because when
 * it is printing a set of rows in <code>JTable.PrintMode.NORMAL</code> and the
 * implementation determines a need to distribute columns across pages,
 * it assumes that all of those rows will fit on each subsequent page needed
 * to fit the columns.
 * <p>
 * It is the responsibility of the developer to ensure that the table is not
 * modified in any way after this <code>Printable</code> is created (invalid
 * modifications include changes in: size, renderers, or underlying data).
 * The behavior of this <code>Printable</code> is undefined if the table is
 * changed at any time after creation.
 *
 * @author Shannon Hickey
 */
class TablePrintable implements Printable {

  /**
   * The table to print.
   */
  private JTable table;

  /**
   * For quick reference to the table's header.
   */
  private JTableHeader header;

  /**
   * For quick reference to the table's column model.
   */
  private TableColumnModel colModel;

  /**
   * To save multiple calculations of total column width.
   */
  private int totalColWidth;

  /**
   * The printing mode of this printable.
   */
  private JTable.PrintMode printMode;

  /**
   * Provides the header text for the table.
   */
  private MessageFormat headerFormat;

  /**
   * Provides the footer text for the table.
   */
  private MessageFormat footerFormat;

  /**
   * The most recent page index asked to print.
   */
  private int last = -1;

  /**
   * The next row to print.
   */
  private int row = 0;

  /**
   * The next column to print.
   */
  private int col = 0;

  /**
   * Used to store an area of the table to be printed.
   */
  private final Rectangle clip = new Rectangle(0, 0, 0, 0);

  /**
   * Used to store an area of the table's header to be printed.
   */
  private final Rectangle hclip = new Rectangle(0, 0, 0, 0);

  /**
   * Saves the creation of multiple rectangles.
   */
  private final Rectangle tempRect = new Rectangle(0, 0, 0, 0);

  /**
   * Vertical space to leave between table and header/footer text.
   */
  private static final int H_F_SPACE = 8;

  /**
   * Font size for the header text.
   */
  private static final float HEADER_FONT_SIZE = 18.0f;

  /**
   * Font size for the footer text.
   */
  private static final float FOOTER_FONT_SIZE = 12.0f;

  /**
   * The font to use in rendering header text.
   */
  private Font headerFont;

  /**
   * The font to use in rendering footer text.
   */
  private Font footerFont;

  /**
   * Create a new <code>TablePrintable</code> for the given
   * <code>JTable</code>. Header and footer text can be specified using the
   * two <code>MessageFormat</code> parameters. When called upon to provide
   * a String, each format is given the current page number.
   *
   * @param table the table to print
   * @param printMode the printing mode for this printable
   * @param headerFormat a <code>MessageFormat</code> specifying the text to be used in printing a
   * header, or null for none
   * @param footerFormat a <code>MessageFormat</code> specifying the text to be used in printing a
   * footer, or null for none
   * @throws IllegalArgumentException if passed an invalid print mode
   */
  public TablePrintable(JTable table,
      JTable.PrintMode printMode,
      MessageFormat headerFormat,
      MessageFormat footerFormat) {

    this.table = table;

    header = table.getTableHeader();
    colModel = table.getColumnModel();
    totalColWidth = colModel.getTotalColumnWidth();

    if (header != null) {
      // the header clip height can be set once since it's unchanging
      hclip.height = header.getHeight();
    }

    this.printMode = printMode;

    this.headerFormat = headerFormat;
    this.footerFormat = footerFormat;

    // derive the header and footer font from the table's font
    headerFont = table.getFont().deriveFont(Font.BOLD,
        HEADER_FONT_SIZE);
    footerFont = table.getFont().deriveFont(Font.PLAIN,
        FOOTER_FONT_SIZE);
  }

  /**
   * Prints the specified page of the table into the given {@link Graphics}
   * context, in the specified format.
   *
   * @param graphics the context into which the page is drawn
   * @param pageFormat the size and orientation of the page being drawn
   * @param pageIndex the zero based index of the page to be drawn
   * @return PAGE_EXISTS if the page is rendered successfully, or NO_SUCH_PAGE if a non-existent
   * page index is specified
   * @throws PrinterException if an error causes printing to be aborted
   */
  public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
      throws PrinterException {

    // for easy access to these values
    final int imgWidth = (int) pageFormat.getImageableWidth();
    final int imgHeight = (int) pageFormat.getImageableHeight();

    if (imgWidth <= 0) {
      throw new PrinterException("Width of printable area is too small.");
    }

    // to pass the page number when formatting the header and footer text
    Object[] pageNumber = new Object[]{Integer.valueOf(pageIndex + 1)};

    // fetch the formatted header text, if any
    String headerText = null;
    if (headerFormat != null) {
      headerText = headerFormat.format(pageNumber);
    }

    // fetch the formatted footer text, if any
    String footerText = null;
    if (footerFormat != null) {
      footerText = footerFormat.format(pageNumber);
    }

    // to store the bounds of the header and footer text
    Rectangle2D hRect = null;
    Rectangle2D fRect = null;

    // the amount of vertical space needed for the header and footer text
    int headerTextSpace = 0;
    int footerTextSpace = 0;

    // the amount of vertical space available for printing the table
    int availableSpace = imgHeight;

    // if there's header text, find out how much space is needed for it
    // and subtract that from the available space
    if (headerText != null) {
      graphics.setFont(headerFont);
      hRect = graphics.getFontMetrics().getStringBounds(headerText,
          graphics);

      headerTextSpace = (int) Math.ceil(hRect.getHeight());
      availableSpace -= headerTextSpace + H_F_SPACE;
    }

    // if there's footer text, find out how much space is needed for it
    // and subtract that from the available space
    if (footerText != null) {
      graphics.setFont(footerFont);
      fRect = graphics.getFontMetrics().getStringBounds(footerText,
          graphics);

      footerTextSpace = (int) Math.ceil(fRect.getHeight());
      availableSpace -= footerTextSpace + H_F_SPACE;
    }

    if (availableSpace <= 0) {
      throw new PrinterException("Height of printable area is too small.");
    }

    // depending on the print mode, we may need a scale factor to
    // fit the table's entire width on the page
    double sf = 1.0D;
    if (printMode == JTable.PrintMode.FIT_WIDTH &&
        totalColWidth > imgWidth) {

      // if not, we would have thrown an acception previously
      assert imgWidth > 0;

      // it must be, according to the if-condition, since imgWidth > 0
      assert totalColWidth > 1;

      sf = (double) imgWidth / (double) totalColWidth;
    }

    // dictated by the previous two assertions
    assert sf > 0;

    // This is in a loop for two reasons:
    // First, it allows us to catch up in case we're called starting
    // with a non-zero pageIndex. Second, we know that we can be called
    // for the same page multiple times. The condition of this while
    // loop acts as a check, ensuring that we don't attempt to do the
    // calculations again when we are called subsequent times for the
    // same page.
    while (last < pageIndex) {
      // if we are finished all columns in all rows
      if (row >= table.getRowCount() && col == 0) {
        return NO_SUCH_PAGE;
      }

      // rather than multiplying every row and column by the scale factor
      // in findNextClip, just pass a width and height that have already
      // been divided by it
      int scaledWidth = (int) (imgWidth / sf);
      int scaledHeight = (int) ((availableSpace - hclip.height) / sf);

      // calculate the area of the table to be printed for this page
      findNextClip(scaledWidth, scaledHeight);

      last++;
    }

    // create a copy of the graphics so we don't affect the one given to us
    Graphics2D g2d = (Graphics2D) graphics.create();

    // translate into the co-ordinate system of the pageFormat
    g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());

    // to save and store the transform
    AffineTransform oldTrans;

    // if there's footer text, print it at the bottom of the imageable area
    if (footerText != null) {
      oldTrans = g2d.getTransform();

      g2d.translate(0, imgHeight - footerTextSpace);

      printText(g2d, footerText, fRect, footerFont, imgWidth);

      g2d.setTransform(oldTrans);
    }

    // if there's header text, print it at the top of the imageable area
    // and then translate downwards
    if (headerText != null) {
      printText(g2d, headerText, hRect, headerFont, imgWidth);

      g2d.translate(0, headerTextSpace + H_F_SPACE);
    }

    // constrain the table output to the available space
    tempRect.x = 0;
    tempRect.y = 0;
    tempRect.width = imgWidth;
    tempRect.height = availableSpace;
    g2d.clip(tempRect);

    // if we have a scale factor, scale the graphics object to fit
    // the entire width
    if (sf != 1.0D) {
      g2d.scale(sf, sf);

      // otherwise, ensure that the current portion of the table is
      // centered horizontally
    } else {
      int diff = (imgWidth - clip.width) / 2;
      g2d.translate(diff, 0);
    }

    // store the old transform and clip for later restoration
    oldTrans = g2d.getTransform();
    Shape oldClip = g2d.getClip();

    // if there's a table header, print the current section and
    // then translate downwards
    if (header != null) {
      hclip.x = clip.x;
      hclip.width = clip.width;

      g2d.translate(-hclip.x, 0);
      g2d.clip(hclip);
      header.print(g2d);

      // restore the original transform and clip
      g2d.setTransform(oldTrans);
      g2d.setClip(oldClip);

      // translate downwards
      g2d.translate(0, hclip.height);
    }

    // print the current section of the table
    g2d.translate(-clip.x, -clip.y);
    g2d.clip(clip);
    table.print(g2d);

    // restore the original transform and clip
    g2d.setTransform(oldTrans);
    g2d.setClip(oldClip);

    // draw a box around the table
    g2d.setColor(Color.BLACK);
    g2d.drawRect(0, 0, clip.width, hclip.height + clip.height);

    // dispose the graphics copy
    g2d.dispose();

    return PAGE_EXISTS;
  }

  /**
   * A helper method that encapsulates common code for rendering the
   * header and footer text.
   *
   * @param g2d the graphics to draw into
   * @param text the text to draw, non null
   * @param rect the bounding rectangle for this text, as calculated at the given font, non null
   * @param font the font to draw the text in, non null
   * @param imgWidth the width of the area to draw into
   */
  private void printText(Graphics2D g2d,
      String text,
      Rectangle2D rect,
      Font font,
      int imgWidth) {

    int tx;

    // if the text is small enough to fit, center it
    if (rect.getWidth() < imgWidth) {
      tx = (int) ((imgWidth - rect.getWidth()) / 2);

      // otherwise, if the table is LTR, ensure the left side of
      // the text shows; the right can be clipped
    } else if (table.getComponentOrientation().isLeftToRight()) {
      tx = 0;

      // otherwise, ensure the right side of the text shows
    } else {
      tx = -(int) (Math.ceil(rect.getWidth()) - imgWidth);
    }

    int ty = (int) Math.ceil(Math.abs(rect.getY()));
    g2d.setColor(Color.BLACK);
    g2d.setFont(font);
    g2d.drawString(text, tx, ty);
  }

  /**
   * Calculate the area of the table to be printed for
   * the next page. This should only be called if there
   * are rows and columns left to print.
   *
   * To avoid an infinite loop in printing, this will
   * always put at least one cell on each page.
   *
   * @param pw the width of the area to print in
   * @param ph the height of the area to print in
   */
  private void findNextClip(int pw, int ph) {
    final boolean ltr = table.getComponentOrientation().isLeftToRight();

    // if we're ready to start a new set of rows
    if (col == 0) {
      if (ltr) {
        // adjust clip to the left of the first column
        clip.x = 0;
      } else {
        // adjust clip to the right of the first column
        clip.x = totalColWidth;
      }

      // adjust clip to the top of the next set of rows
      clip.y += clip.height;

      // adjust clip width and height to be zero
      clip.width = 0;
      clip.height = 0;

      // fit as many rows as possible, and at least one
      int rowCount = table.getRowCount();
      int rowHeight = table.getRowHeight(row);
      do {
        clip.height += rowHeight;

        if (++row >= rowCount) {
          break;
        }

        rowHeight = table.getRowHeight(row);
      } while (clip.height + rowHeight <= ph);
    }

    // we can short-circuit for JTable.PrintMode.FIT_WIDTH since
    // we'll always fit all columns on the page
    if (printMode == JTable.PrintMode.FIT_WIDTH) {
      clip.x = 0;
      clip.width = totalColWidth;
      return;
    }

    if (ltr) {
      // adjust clip to the left of the next set of columns
      clip.x += clip.width;
    }

    // adjust clip width to be zero
    clip.width = 0;

    // fit as many columns as possible, and at least one
    int colCount = table.getColumnCount();
    int colWidth = colModel.getColumn(col).getWidth();
    do {
      clip.width += colWidth;
      if (!ltr) {
        clip.x -= colWidth;
      }

      if (++col >= colCount) {
        // reset col to 0 to indicate we're finished all columns
        col = 0;

        break;
      }

      colWidth = colModel.getColumn(col).getWidth();
    } while (clip.width + colWidth <= pw);

  }

}
